import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;

/**
 * 
 * Authors - Oren Bracha, Rony Lahav and Yehuda Eterman
 *
 * this is the main class in the system
 * 
 * runs the commands
 */
public class proj350 {
	public static int CONN_ESTABLISH_ATTEMPTS_NUMBER = 4;
	public static int UDP_ATTEMPTS_NUMBER = 3;
	public static int TIMEOUT_IDLE_PERIOD = 2000;
	
	public static int MANAGER_REFRESH_TIME = 5000;
	
	private static int mss;				// Mss value
	private static int updateFrequency; // Update frequency value (in seconds)
	private static int Nparameter;		// N parameter value
	
	private static DBTuple myTuple = null;
	
	public static Thread tcpManagerThread=null;
	public static Thread udpManagerThread=null;
	public static Thread updaterThread=null;
	
	LinkedList<ICmd> commandsList = InputFileManager.getInstance().getCommandsValues();
	
	private static long threadUniqueId=0;
	private static HashMap<Long, Thread> livingThreads = new HashMap<Long,Thread>();
	
	private static int bufferSize;	// buffer size value
	public static int rcvWin;		// rcvWin value
	
	public static boolean shutDown = false; 
	public static boolean inTrialMode = false;
	public static int sleepPeriod = 0;
	public static int runPeriod = 0;
	
	private static proj350 peer = null;
	
	public static String inputFilePath=null;
	public static String configFilePath=null;
	public static String outputFilePath=null;
	
	private proj350(){
	}
	
	//singleton
	public static proj350 getInstance()
	{
		if (peer == null) {
			peer = new proj350();
		}
		return peer;
	}
	
	/**
	 * reads the peer data from the input file or the DB 
	 * starts 
	 * @return
	 */
	private boolean initializePeerData() {
		boolean connEstablished = false;
		
		// Get peer data from input file
		mss = Integer.parseInt(InputFileManager.getInstance().getEnvironmentValue("mss"));
		updateFrequency = Integer.parseInt(InputFileManager.getInstance().getEnvironmentValue("updatefrequency"));
		Nparameter = Integer.parseInt(InputFileManager.getInstance().getEnvironmentValue("N"));

		// Create Listeners
		TCPManager tcpManager = new TCPManager();
		tcpManagerThread = new Thread(tcpManager);
		
		UDPManager udpManager = new UDPManager();
		udpManagerThread = new Thread(udpManager);
		
		Updater updater = new Updater();
		updaterThread = new Thread(updater);

		//starting the managers for the connections TCP and UDP
		if (!DataBase.getInstance().isDBEmpty()){
			myTuple = DataBase.getInstance().findTuple(InputFileManager.getInstance().getPeerDataValue("peername"));
			bufferSize = rcvWin = Integer.parseInt(InputFileManager.getInstance().getPeerDataValue("buffer"));
			DataBase.getInstance().awakenAllPeers();
			
			// Start Listeners and updater
			tcpManagerThread.start();
			udpManagerThread.start();
			updaterThread.start();
		}
		else{
			//initializing peer from input file
			connEstablished = newPeer();
			
			if (connEstablished) {
				// After first connection established wait for update or terminate
				while (proj350.inTrialMode && !shutDown){
					try{
						// Give the listener a chance to get an update
						Thread.sleep(1000);
					}
					catch (InterruptedException e){
					}
				}
			}
		}
		return !(connEstablished && proj350.inTrialMode);
	}
	
	/**
	 * initializing new peer from input file and connecting to another peer
	 * @return true if initialize is correct
	 */
	private boolean newPeer() {
		inTrialMode = true;
		boolean connectionEstablished = false;
		String globalTargetPeerName="";
		
		try {
			InputFileManager IFMinstance = InputFileManager.getInstance();
			if (IFMinstance != null) {
				
				// Read peer data from file
				String peername = IFMinstance.getPeerDataValue("peername");
				String ip = IFMinstance.getPeerDataValue("ip");
				String tcp = IFMinstance.getPeerDataValue("tcp");
				String udp = IFMinstance.getPeerDataValue("udp");
				String phone = IFMinstance.getPeerDataValue("phone");
				String email = IFMinstance.getPeerDataValue("email");
				String buffer = IFMinstance.getPeerDataValue("buffer");
				String pic = IFMinstance.getPeerDataValue("pic");
				String picsize = IFMinstance.getPeerDataValue("picsize");
				
				// Create DBTuple with the peer's data 
				DBTuple newTuple = new DBTuple();
				newTuple.setPeerName(peername);
				newTuple.setIp(ip);
				newTuple.setTcpPort(tcp);
				newTuple.setUdpPort(udp);
				newTuple.setPhoneNum(phone);
				newTuple.setEmailAddress(email);
				newTuple.setPictureFile(pic);
				newTuple.setPicSize(picsize);
				String currTime = DateStringConverter.timeCurrentForMessages();
				newTuple.setLastConnectionTime(currTime);
				newTuple.setPeerVersion(currTime);
				newTuple.setPeerCommitTime(currTime);
				newTuple.setDbVersion(currTime);
				newTuple.setLastConnectionTime(currTime);
				newTuple.setPicVersion(currTime);
				newTuple.setStatus("1");
				newTuple.setRcvWin(buffer);
				
				bufferSize = Integer.parseInt(buffer);
				rcvWin = bufferSize;
				DataBase.getInstance().updateTuple(newTuple);
				
				myTuple = newTuple;
				
				tcpManagerThread.start();
				udpManagerThread.start();
			
				ConfigFileManager CFMinstance = ConfigFileManager.getInstance();
				if (CFMinstance != null) {
					// Reading another oeer data from config file
					String targetPeerName = globalTargetPeerName = CFMinstance.getConfigVarValue("peername");
					String targetIP = CFMinstance.getConfigVarValue("ip");
					String targetTcpPort = CFMinstance.getConfigVarValue("tcp");
					String targetUdpPort = CFMinstance.getConfigVarValue("udp");
			

					//trying to connect to another peer using UDP
					connectionEstablished = initialUDPConnection(targetPeerName, targetIP, targetUdpPort); 

					//if cannot connect with UDP try with TCP
					if (!connectionEstablished) {
						Socket socket = initialTCPConnection(targetIP, targetTcpPort);
						connectionEstablished = true;
						//creating TCP connection
						TCPSenderConnection newConn = new TCPSenderConnection(socket,targetPeerName,targetIP,proj350.getNewUniqueID());
						Thread thread = new Thread(newConn);
						proj350.addThreadToLivingList(newConn.getUniqueID(), thread);
						thread.start();
					}
				}
			}
			
			return connectionEstablished;
			 
		}
		catch (IOException e1) {
			Logger.getInstance().printErrorNotAvailable(globalTargetPeerName);
			return false;
		}
	}
	
	/**
	 * initial connection using UDP to the peer in the config file
	 * 3 tries using UDP
	 * @return true of connection established and data updated
	 */
	private boolean initialUDPConnection(String peerName, String ip, String port) {
		DatagramSocket ds = null;
		boolean udpSucc = false;
		int counter = 0;
		while (counter < proj350.UDP_ATTEMPTS_NUMBER && !udpSucc){
			try{
				//creating socket and packet to send
				ds = new DatagramSocket();
				String updateMessage = MessageCenter.getInstance().createMessage_Update(proj350.getMyTuple().getDbVersion(), proj350.getMyPeerName());
				DatagramPacket dp = new DatagramPacket(updateMessage.getBytes(), updateMessage.length(), InetAddress.getByName(ip),Integer.parseInt(port));
				
				//trying to send packet
				ds.send(dp);
				++counter;
				
				Logger.getInstance().printInOutMsg(false, updateMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(ds.getLocalPort()), peerName, ip, port, "udp");

				//setting timeout and waiting for reply from the peer
				ds.setSoTimeout(proj350.TIMEOUT_IDLE_PERIOD);
				byte[] buffer = new byte[1024];
				DatagramPacket serverPacket = new DatagramPacket(buffer,buffer.length);
				ds.receive(serverPacket);
				udpSucc = true;
				int receiverPort = serverPacket.getPort();
				
				//sending the request entry to the sender so it would be processed and return an entry 
				UDPSenderConnection udpSendConn = new UDPSenderConnection(peerName,ip,ds,receiverPort,serverPacket,proj350.getNewUniqueID());
				Thread thread = new Thread(udpSendConn);
				proj350.addThreadToLivingList(udpSendConn.getUniqueID(), thread);
				thread.start();
			}
			catch (SocketTimeoutException e){
				//if there was no answer then we close the program
				if (ds != null && !ds.isClosed()){
					ds.close();
				}
			}
			catch(IOException e){
			}
			catch(Exception e){
			}
		}
		
		return udpSucc;
	}
	
	private Socket initialTCPConnection(String ip, String port) throws IOException{
		return(new Socket(ip,Integer.parseInt(port)));
	}
	

	/**
	 * executing commands from the input file  
	 */
	public void runRegularFlow(){
		for (int i=0; i<commandsList.size() && !shutDown; ++i){
			ICmd currCmd = commandsList.get(i);
			currCmd.executeCmd();
			
			// Check if got sleep command
			if (sleepPeriod > 0){
				try{
					// Killing all of the "small" threads (the threads that handles sessions)
					Collection<Thread> c = livingThreads.values();
					for (Thread thread : c) {
						thread.stop();
					}
					Thread.sleep(sleepPeriod*1000);
					sleepPeriod = 0;
				}
				catch(InterruptedException e){
					sleepPeriod = 0;
				}
			}
			// Check if got run command
			else if (runPeriod > 0){
				try{
					Thread.sleep(runPeriod*1000);
					runPeriod = 0;
				}
				catch(InterruptedException e){
					runPeriod = 0;
				}
			}
		}
	}
	
	public static int getUpdateFrequency() {
		return updateFrequency;
	}
	
	public static void setUpdateFrequency(int updateFreq) {
		updateFrequency = updateFreq;
	}
	
	public static int getNParameter() {
		return Nparameter;
	}

	public static void setNParameter(int NParam) {
		Nparameter = NParam;
	}
	
	public static int getMSS() {
		return mss;
	}
	
	public static void setMSS(int MSS) {
		mss = MSS;
	}
	
	public static boolean isShutDown() {
		return shutDown;
	}
	
	public static String getMyPeerName() {
		return myTuple.getPeerName();
	}
	
	public static String getMyIp() {
		return myTuple.getIp();
	}
	
	public static int getMyTCPPort() {
		return Integer.parseInt(myTuple.getTcpPort());
	}
	
	public static int getMyUDPPort() {
		return Integer.parseInt(myTuple.getUdpPort());
	}
	
	public static int getSleepPeriod() {
		return sleepPeriod;
	}
	
	public static DBTuple getMyTuple() {
		return myTuple;
	}
	
	public static void setMyTuple(DBTuple tuple) {
		myTuple = tuple;
	}
	
	public static long getNewUniqueID(){
		++threadUniqueId;
		return threadUniqueId;
	}
	
	public static void addThreadToLivingList(long key, Thread thread){
		livingThreads.put(key, thread);
	}
	
	public static void removeThreadFromLivingList(long key){
		livingThreads.remove(key);
	}
	
	public void run() {
	}

	/**
	 * running the program
	 */
	public void start(){
		try{
			//trying to initialize peer and then running the commands
			if (initializePeerData()) {
				runRegularFlow();
			}
		}
		catch(Exception e){
			shutDown = false;
		}
		finally{
			//ensuring that the managers get closed
			shutDown = true;
			if (tcpManagerThread != null && tcpManagerThread.isAlive()){
				tcpManagerThread.stop();
			}
			
			if (udpManagerThread != null && udpManagerThread.isAlive()){
				udpManagerThread.stop();
			}
			if (updaterThread != null && updaterThread.isAlive()){
				updaterThread.stop();
				}
				
			Logger.getInstance().close();
		}
	}
	
	//tries to allocate free space from the rcvWin - true if succeeded false if not
	public synchronized boolean allocateRcvWinSpace(int space){
		if (rcvWin-space>=0){
			rcvWin-=space;
			return true;
		}else{
			return false;
		}
	}
	
	/**
	 * 
	 * @param space
	 * @return true if succeeded in freeing the rcvWin 
	 */
	public synchronized boolean freeRcvWinSpace(int space){
		if (rcvWin+space<=bufferSize){
			rcvWin+=space;
			return true;
		}
		else{
			rcvWin=bufferSize;
			return true;
		}
	}
	
	/**
	 * creating peer and running the program
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		inputFilePath = args[0];
		configFilePath = args[1];
		outputFilePath = args[2];
		System.setProperty("java.net.preferIPv4Stack", "true");
		DataBase.getInstance().LoadDBFromFile("db.txt");
		proj350 peer = proj350.getInstance();
		peer.start();
		DataBase.getInstance().SaveDBToFile("db.txt");
		System.exit(1);
	}
}
